LOADING...

加载过慢请开启缓存(浏览器默认开启)

loading

Bonbon

Go farther and see the brighter light

desktop-robot

2022/7/20

桌面机器人

好久没有写博客了,今天有时间整理说说这学期的项目课。

项目灵感来自于稚晖君的电脑配件:https://www.bilibili.com/video/BV1ka411b76m


上学期,由于一些焊接问题,自制SOC失败,所以要使得产品有高性能的计算就必须要有上位机。

而现成的树莓派等上位机,体积仍然很大,很难集成到小型产品内部,所以上学期的项目课总体就做得很大。

受稚晖君的启发,采用USB外接高速Phy的方式通信,将所需图像以及其他数据,

通过通信的方式传到电脑,以电脑来作为计算的媒介。

想法很好,这样就可以将项目集成得很小,而且后续都是电脑开发,定制化程度高。

接下来简单回顾一下制作的过程。


USB通信

一开始,我是对USB通信完全没有了解的。

陷入了USB的HID(Human Interface Device)的路,后续才发现只需要采用虚拟串口就可以了。

更多的是电路焊接bug,各种虚焊识别不到。

不用钢网手涂锡膏,用风枪焊好后属于一种半虚不虚的状态,用久了就会虚,焊接能力还是不行

详细实现写在了这篇博客:https://www.bonbonn.shop/2022/04/10/stm32-usb-vpc-hs/


IIC舵机

通信调通之后,我就开始尝试复现稚晖君的舵机板

项目链接:https://github.com/peng-zhihui/ElectronBot/tree/main/1.Hardware/ServoDrive

但是我发现,需要从电机引两条线以及电位器引三条线焊到板子,最后成型的效果不是很美观,如下:

所以我就尝试修改板子的形状,使得板子能够像原舵机板一样,

将电机口和电位器口的位置对应到板子上,像插件一样集成到舵机内部。

第一次尝试把板子压缩到最小,从9.6 ​ ×\times 18.4 进一步优化成 9 ×\times 17.6 (毫米制)。

最终实物焊接到舵机上的效果如下:

已经是较为满意的结果了,虽然还是大了一点点,舵机的盖子盖不上。

(不过最终因为IIC通信等一些问题,舵机板也没有最终被用上,代码也没在后续板上验证)

详细实现写在了这篇博客:https://www.bonbonn.shop/2022/07/20/iic-servo/


USB传图片

因为打算用python进行后续开发,所以通信也是用python。

虚拟串口很方便,方便在于你只需要当做一个串口使用即可,并且可以无视波特率。

通过图像位数转换后(博客链接:https://www.bonbonn.shop/2022/07/20/image-digit-conversion/

将数据传给单片机进行显示屏的显示即可。

思路很简单,但是实现起来就出现了一些问题。

比如图像数据太大,用uint8_t的数组存储图像数据,数组大小为240*240*2=115200

会出现raw溢出的现象,因为target中默认是0x1c000 即114688

需要把IRAW1调大,把IRAW2调小

又比如帧率不够,图像长宽均为240,总数据量为240*240*16=921600bit,单片机显示屏显示速率不够

(USB高速通信速率可以达到几十甚至几百Mbps,通信速率是肯定够的)

第一个优化的想法就是,传的数据是正方形的图片,但是显示只需要显示圆形区域,

不过代码优化后发现帧率还是不能满足实时性。

后来发现商家提供的显示屏显示代码是软件实现SPI,我换成了硬件实现SPI,速率瞬间就上去了。


动态表情

表情制作就不多说了,用PS绘制关键部位,然后用Ae制作动画,放一张眨眼关键帧图:

阅读全文

image_digit_conversion

2022/7/20

Python图片位数转换

一般购买嵌入式的屏幕,商家都会提供软件,可以将bmp文件转换成对应的C数组。

我们只需要提前转换好后放入代码中并烧录进单片机。

其实商家提供的软件只是做了简单的一件事,就是扫描并将图像位数进行转换。

我们常规使用的png图像是32位的,有4个通道,RGB三个通道以及一个α\alpha透明通道。

我们常规使用的jpeg以及bmp图像是24位的,只有RGB三个通道。

而我们使用的屏幕是16位的,所以需要进行位数的转换。


具体来说,24位bmp图像是RGB888

我们需要转换成16位的RGB565

举个例子 比如24位的像素RGB888是11010110 11010100 11101010(高位在前)

转换成16位的像素RGB565则只需要取R的高五位,G的高六位,B的高五位,如下图:

以python实现为例,假设输入的是一个24位的数,转换实现如下:

def rgb888_to_rgb565(rgb888):
    
    # 获得rgb各自对应的八位
    r = (rgb888 & 0xff0000) >> 16
    g = (rgb888 & 0x00ff00) >> 8
    b =  rgb888 & 0x0000ff

    # 888 -> 565
    r >>= 3
    g >>= 2
    b >>= 3

    return r << 11 | g << 5 | b

而python用opencv读取图像,是一个numpy数组,RGB通道各自的值是分离的。

假设我们模拟软件,将一个bmp图像转换成对应的16位c数组,python实现如下:

import numpy as np
import cv2 as cv


def img_rgb888_to_rgb565(img888):

    # 这里img是8位的 需要转类型到16位 不修改后续左移会溢出
    img888 = img888.astype(np.uint16)

    # opencv 是 BGR
    b = img888[:, :, 0]
    g = img888[:, :, 1]
    r = img888[:, :, 2]

    # 888 -> 565
    r >>= 3
    g >>= 2
    b >>= 3

    return r << 11 | g << 5 | b


def bit16_to_two_bit8(bit16):

    #高八位和低八位
    high_bit8 = (bit16 & 0xff00) >> 8
    low_bit8 = bit16 & 0x00ff

    return high_bit8, low_bit8


def write_list_to_c_file(data, save_path, array_type, array_name):

    with open(save_path, "w") as f:

        # 第一行 写类型和数组名
        f.write(array_type + " " + array_name + "[" + str(len(data)) + "]" + " = {")
        f.write('\n')

        # 循环写数据
        for i in range(len(data)):
            f.write(data[i])
            f.write(",")

            if i % 16 == 15:
                f.write("\n")

        # 尾行
        f.write("};")
        f.write("\n")


if __name__ == "__main__":
    img = cv.imread(r"./1.bmp")
    rgb565 = img_rgb888_to_rgb565(img)
    high, low = bit16_to_two_bit8(rgb565)
    bit16_array = np.concatenate((np.expand_dims(high, 2), np.expand_dims(low, 2)), axis=2)  # 高位在前
    data_list = list(map(lambda x: ("0X%02X" % x), bit16_array.flatten().tolist())) #flatten实现了横向扫描 要纵向扫描可以先转置
    write_list_to_c_file(data=data_list, save_path="./test.c", array_type="const unsigned char", array_name="gImage_1")

实现起来比较容易,但是有挺多需要注意的地方,我写在了注释


参考:https://bbs.csdn.net/topics/360140415

参考:https://blog.csdn.net/baidu_26678247/article/details/65629447

阅读全文

iic_servo

2022/7/20

复现IIC舵机

稚晖君将普通舵机魔改成了IIC舵机,尝试从原理上复现一下

项目地址:https://github.com/peng-zhihui/ElectronBot/tree/main/1.Hardware/ServoDrive


电路原理

拆开舵机可以发现,里面主要是电机,电位器,齿轮组。

简单来讲实现闭环就是电机输出,齿轮组传动,读电位器位置,闭环。


稚晖君设计的舵机板,需要拆开舵机底座,替换掉电路板

将舵机板的A、V、G口对应连接电位器的ADC采样、VCC、GND口、舵机板的OUT1、OUT2口对应电机输出。

主要实现功能是外部发送期望角度主控芯片闭环控制电机以及电机转动时将角度数据发送给外部。


电位器引脚确定

电位器可以理解成滑动变阻器,有三个引脚,其中两个脚的阻值不随转动变化。


对于舵机,我们只能通过拧舵机进而调节电位器,需要安装好舵盘。

而舵机拧到底也不一定对应电位器拧到底,故用电表笔通断档测是不行的,需要用电阻档。

把舵盘沿任一方向拧到底,分别记录三个引脚各自的电压。

把舵盘沿反方向拧到底,分别记录三个引脚各自的电压。

假设三个引脚为ABC,我测量的电压如下:

AB AC BC
一个方向拧到底 0.003千欧 5.1千欧 5.1千欧
另一个方向拧到底 4.97千欧 5.1千欧 0.2千欧

这样就可以判断B脚是阻值变化脚了,而另外两个不关键,正反接决定了不同转向对应电阻的增大还是减少。

电机驱动采用FM116B,输入两个PWM,输出两个信号。

这两个输出引脚接到电机的两个引脚,同样正反接决定了正反转。


FM116B逻辑如下:

IN1 IN2 OUT1 OUT2 功能
L L Z Z 停止
H L H L 正转
L H L H 反转
L L L L 刹车

软件实现

用到了F0芯片,要安装芯片包,链接:https://www.keil.com/dd2/Pack/

配置ADC和PWM,测试确定一下电位器的增减方向以及电机的正反转方向。

PID的控制就较为简单了,电位器的最大值和最小值对应着角度的最大值和最小值。

控制所需的角度值,转化为对应的电位器的值,作为PID的偏差进行控制即可。

放一段主要代码:

while (1)
{
	if(HAL_OK == HAL_ADC_PollForConversion(&hadc, 50))
	{
		now_value = HAL_ADC_GetValue(&hadc);
		target_value = Angle2Potentiometer(target,0.0,180.0,Potentiometer_min,Potentiometer_max);
		PID_Calc(Servo_PID_ptr,now_value,target_value,500);
		if(Servo_PID_ptr->Output>0)
		{
			TIM3->CCR1 = 0;
			TIM3->CCR2 = Servo_PID_ptr->Output;
		}
		else
		{
			TIM3->CCR1 = -Servo_PID_ptr->Output;
			TIM3->CCR2 = 0;
		}		
	}
	
	HAL_Delay(1);
}
阅读全文

config_opencv_in_windows

2022/5/30

windows下opencv配置

最近有个大作业,有需求要在QT下使用opencv

原本想着先将opencv编译完成,然后在vscode上验证,最后直接在QT内配置即可

结果是不同的编译器编译出来的opencv是不一样的

比如要在QT下使用opencv,就要用QT的编译代码的编译器去编译opencv

所以才有了下述的vscode和QT配置两个环节。


VScode

编译

下载opencv源码,选择sources下载

地址:https://opencv.org/releases


安装CMake,选择installer下载

地址:https://cmake.org/download/


打开CMake-gui,选择源码路径,新建build为编译路径,配置用MinGW编译

第一次会出现红色警告信息,再按一次Configure就好了,没问题就可以Generate生成Makefile

然后在编译路径打开命令行,开始编译

mingw32-make

我在编译过程中报错:gcc: error: long: No such file or directory

解决:CMake配置中取消OPENCV_ENABLE_ALLOCATOR_STATS勾选


编译完成后就可以安装

mingw32-make install

安装成功后会出现一个install文件夹,这个install文件夹内就包含后续所需的文件

把install下的x64/mingw/bin路径加入环境变量,重开机


测试

测试代码:

#include "opencv2/opencv.hpp"
#include "iostream"

int main(void)
{
    cv::Mat img = cv::imread("图片的绝对路径"); 
	if (img.empty())
	{
		std::cout << "error!" << std::endl;
		return -1;
	}
		
	cv::imshow("", img);
	cv::waitKey(0);
	cv::destroyAllWindows();
	return 0;
}

假设上述编译安装install的路径为path,则我们可以通过以下命令编译

g++ -g main.cpp -o main.exe -I path/install/include -L path/install/x64/mingw/lib -l opencv_core430 -l opencv_imgcodecs430 -l opencv_highgui430

-I指定头文件目录,-L指定库目录,-l为链接库(最后三位数字表示版本,我的版本为4.30)

我通过命令行可以正常编译生成main.exe,但是运行就会有无法定位程序输入点等问题


然后我尝试将上述命令配置进vscode,也很容易,主要就是配置tasks.json

发现要指定当前工作目录为mingw64的路径,程序才能正常运行

"cwd": "F:/mingw-w64/mingw64/bin"

而这样子设置随之带来的就是代码里的路径都要设置为绝对路径(比如图片路径)

而且用c++的_getcwd()函数,获得的也是上述路径

还好只是用于验证环境,要是这样子拿来开发估计会崩溃


QT

以下编译过程最终没有使用,出现了QT和Opencv版本不均的问题

编译

要在QT内使用opencv,就要用QT编译的编译器编译opencv,有点拗口hh

QT使用的编译器在QT的安装目录下可以看见

例如我的是F:\QT5.9.0\Tools\mingw530_32\bin

需要将这个路径添加到环境变量中,然后在cmake中选择这个编译器编译


编译过程出现下列两个问题:

出错:member CMakeFiles\opencv_core.dir/objects.a(vs_version.rc.obj) in archive is not an object

解决方法:添加OPENCV_VS_VERSIONINFO_SKIP

出错:undefined reference to `_imp__opj_image_destroy@4’

解决方法:去除勾选WITH_OPENJPEG


编译完同样install,然后把install下的x64/mingw/bin文件夹加入环境变量,重开机


测试

测试代码:

#include "mainwindow.h"
#include <QApplication>
#include <opencv2/opencv.hpp>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
//    MainWindow w;

    cv::Mat image = cv::imread("图片的绝对路径");
    if (image.empty())
    {
        std::cout << "error" << "\n";
        return -1;
    }

    cv::imshow("show", image);
    cv::waitKey();


//    w.show();

    return a.exec();
}

同样假设编译安装install的路径为path,需要再在QT的.pro文件中加入

INCLUDEPATH += path/install/include/
LIBS += path/install/x86/mingw/lib/libopencv_*.a

测试的时候发现图片没法正常显示,一直显示灰色的,鼠标放上去就在转圈圈

怀疑可能是版本不兼容的问题(Opencv版本太高而QT版本太低)


为了快速验证想法(其实也是懒得再编译一次),直接下载用同样QT编译器编译生成的文件

下载链接:https://github.com/huihut/OpenCV-MinGW-Build

我的QT的编译器是mingw530_32,可以找到对应的文件

发现对应的opencv版本是3.4.5,一定程度上验证了版本差别太大的想法


下载完成后,同样把bin加入环境变量(顺便把刚刚编译后不能使用的环境变量删掉),重开机

然后同样在QT的.pro加入对应路径的include和lib

正常运行√

而且没有存在上述vscode中的路径错误的问题

阅读全文

learn_pangolin

2022/5/28

Pangolin笔记

最近有个大作业,需要写个界面,显示用C++的Opencv实现的一些效果

久有耳闻Pangolin,打算用它试试实现,顺便入个门

但是最终因为他的控件只有竖直方向排版(也可能是我了解不深),而且我也没有解决中文乱码的问题

没法满足需求,最终还是打算用QT做,记录一下学习过程


使用Pangolin的感觉就是资料太少了,只有一些零零碎碎的demo

Pangolin的笔记以注释的形式写在代码里(有些不用使用的代码注释掉了作为备忘)

main.cpp

#include <pangolin/pangolin.h>
#include <opencv2/opencv.hpp>

#define pix(x) (pangolin::Attach::Pix(x)) //方便以像素定义画面尺寸

//窗口大小
const short WINDOWS_WIDTH = 640;
const short WINDOWS_HEIGTH = 480;

//上侧选择栏大小
const short TOP_SELECTION_WIDTH = WINDOWS_WIDTH;
const short TOP_SELECTION_HEIGHT = 35;

//左侧选择栏大小
const short LEFT_SELECTION_WIDTH = 128;
const short LEFT_SELECTION_HEIGHT = WINDOWS_HEIGTH - TOP_SELECTION_HEIGHT;

//显示图像大小
const short IMAGE_WIDTH =  WINDOWS_WIDTH - LEFT_SELECTION_WIDTH;
const short IMAGE_HEIGHT =  WINDOWS_HEIGTH - TOP_SELECTION_HEIGHT;

void function()
{
    std::cout << "In the function" << std::endl;
}

int main()
{ 
    //创建窗口 窗口名 宽 高
    pangolin::CreateWindowAndBind("Main",WINDOWS_WIDTH,WINDOWS_HEIGTH);

    //设置字体样式
    // pangolin::GlFont * font = new pangolin::GlFont("../font/simhei.ttf", 60.0);

    //打开颜色混合 以及颜色混合方式 省略则字体无法正常显示
    // glEnable(GL_BLEND);
    // glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    
    //开启深度测试 窗口中只会绘制面朝相机的那一面像素 绘制3D就要打开
    // glEnable(GL_DEPTH_TEST);

    //设置相机
    // ProjectionMatrix指定内参 (高 宽 fx fy cx cy 最近视距 最远视距)
    // ModelViewLookAt指定外参 相机所在x,y,z 视点x,y,z 相机轴
    // pangolin::OpenGlRenderState camera(
    //         pangolin::ProjectionMatrix(640,480,420,420,320,320,0.2,100),
    //         pangolin::ModelViewLookAt(2,0,2, 0,0,0, pangolin::AxisY));

    //设置视图
    // .SetBounds 设置位置大小属性 分别对应(bottom, top, left, right)的比例 也可以用pangolin::Attach::Pix(x)指定像素 
    // pangolin::View& camera_view = pangolin::CreateDisplay()
    //         .SetBounds(0.0, 1.0, 0.0, 1.0, 640.0f/480.0f)
    //         .SetHandler(new pangolin::Handler3D(camera));
    
    // 左侧选择栏
    pangolin::CreatePanel("left_selection").SetBounds(0, pix(LEFT_SELECTION_HEIGHT), 0, pix(LEFT_SELECTION_WIDTH)); 
    pangolin::Var<bool> left_button1("left_selection.Button", false, false);// name 默认值 (toggle为true为选框 为false为按钮)
    pangolin::Var<std::function<void(void)>> left_button2("left_selection.function", function);//按钮回调函数

    // 顶部选择栏
    pangolin::CreatePanel("top_selection").SetBounds(pix(WINDOWS_HEIGTH-TOP_SELECTION_HEIGHT), pix(WINDOWS_HEIGTH), 0, pix(WINDOWS_WIDTH)); 
    pangolin::Var<bool> top_button1("top_selection.Button", false, false);// name 默认值 (toggle为true为选框 为false为按钮)

    // 图片
    pangolin::View& image_view = pangolin::Display("rgb").SetBounds(0, pix(IMAGE_HEIGHT), pix(WINDOWS_WIDTH - IMAGE_WIDTH), pix(LEFT_SELECTION_WIDTH+IMAGE_WIDTH));// 建立图片视图
    pangolin::GlTexture imageTexture(IMAGE_WIDTH,IMAGE_HEIGHT); // 图片容器 width height
    cv::Mat image = cv::imread("../test.jpg"); //读取图像
    cv::resize(image,image,cv::Size(IMAGE_WIDTH,IMAGE_HEIGHT));
    imageTexture.Upload(image.data,GL_BGR,GL_UNSIGNED_BYTE);//将图片放入容器 BGR格式 无符号

    while(!pangolin::ShouldQuit()) //如果没退出界面
    {
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 清空颜色和深度缓存

        //显示相机
        // camera_view.Activate(camera);

        if (pangolin::Pushed(left_button1))
        {
            std::cout << "In Button" << std::endl;
        }

        //显示图片
        // image_view.Activate();
        // imageTexture.RenderToViewportFlipY(); // 要翻转y不然图像为上下颠倒 原型为RenderToViewport();

        //显示字体
        // glColor3f(0.0,0.0,1.0); //RGB 蓝色
        // std::string tt = "1234";
        // font->Text(tt.c_str()).Draw(200, 200, 0);

        pangolin::FinishFrame(); //显示
    }

    return 0;
}

CMakeLists.txt

cmake_minimum_required(VERSION 2.8)
project(learning_pangolin) 
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_BUILD_TYPE "Debug")

# 使用opencv
find_package( OpenCV REQUIRED ) 
find_package( Pangolin REQUIRED )
include_directories(${OpenCV_INCLUDE_DIRS}) 

add_executable(${PROJECT_NAME}
	main.cpp
) 
target_link_libraries(${PROJECT_NAME}
	${OpenCV_LIBS} 
	${Pangolin_LIBRARIES}
)

结果如下:

Pangolin中的按钮是绑定在一个控制板panel上的

我的想法是上侧选择栏的按钮横向排列作为界面切换

但是好像Pangolin没有提供,默认按钮绑定panel后就是竖直排列(也就是和panel基本等宽)

如果实在要在上边横向排列按钮的话,应该可以一个按钮一个panel横向排列过去


总结起来就是,我感觉Pangolin适合做一些简单的交互式的3D的可视化

比较适合与类似SLAM可视化等,左边是简单按钮控制一些逻辑,右边是显示3D位姿之类的画面

阅读全文

STM32_standard_to_hal

2022/5/25

STM32标准库转HAL库

虽说现在STM32主流是HAL库,但毕竟标准库存在那么多年,很多遗留下来的代码都是标准库的

我最近买的一个LCD显示屏,商家给的例程就是标准库的,我需要把它移植到使用HAL库的代码中

我先根据实际使用的引脚,修改商家的例程,编译执行,保证执行无误

能够正常执行之后就可以开始修改代码移植了

下面是我移植的一些记录,没有涉及到中断什么的,还是比较简单的

/*改include*/
//将 #include "sys.h"; 改为 #include "main.h";

/*改delay*/
//删掉 #include "delay.h"; 
//将delay_ms(100); 改为 HAL_Delay(100);

/*改type*/
//将u8 u16 u32 分别批量替换为 uint8_t uint16_t uint32_t 

/*改GPIO*/
//GPIO_InitTypeDef GPIO_InitStructure;
//RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
//GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7|GPIO_Pin_11;
//GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
//GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
//GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
//GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
//GPIO_Init(GPIOA, &GPIO_InitStructure);
//GPIO_SetBits(GPIOA,GPIO_Pin_7|GPIO_Pin_11);
//改为:
//GPIO_InitTypeDef GPIO_InitStruct = {0};
//__HAL_RCC_GPIOA_CLK_ENABLE();
//GPIO_InitStructure.Pin = GPIO_PIN_7|GPIO_PIN_11;
//GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP;
//GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH;
//GPIO_InitStructure.Pull = GPIO_PULLUP;
//HAL_GPIO_Init(GPIOA, &GPIO_InitStructure);
//HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7|GPIO_PIN_11, GPIO_PIN_SET);
阅读全文

use_bibtex_to_cite_arxiv

2022/5/14

使用bibtex引用arxiv文献

平常我们用bibtex引用文献,可以去谷歌学术找到对应文章

然后点引用就能自动生成bibtex引用内容(格式要求严格时需要按需修改)

不过很多文章会先在arxiv占坑,然后后续发表在其他期刊

而这些文章在谷歌学术上就会显示是arxiv的,而且引用也是arxiv的

这样其实是不规范的,而且有可能会违反一些参考文献提交的规则

网上已经有现成的项目可以直接搜索指定期刊

并将原本引用arxiv的bibtex文件直接转换成引用期刊的bibtex文件,非常好用!

项目地址:https://github.com/yuchenlin/rebiber

看README配置即可


我是采用下面命令安装的:

pip install rebiber -U

然后指定输入文件路径和输出文件路径即可:

rebiber -i ./ref.bib -o ./output.bib

转换后编译检查一下,会发现还有一些arxiv的文章没有被转换

1、有可能这个文章是只在arxiv发表的,没有发表在其他期刊

2、有可能是这个文章是比较晚发表在期刊上的,不在项目支持的范围内


在arxiv中搜索文章,一般Comments有写最终被接受在哪个期刊

或者谷歌直接搜文章名,也很容易看到期刊官网的这篇文章

这样就能确定arxiv是否在其他期刊发表


如果期刊是较晚发表的,上述项目也支持导入新的期刊列表

大致流程为

1、在dblp中搜索并下载bibtex文件

2、用项目库文件bib2json.py将bibtex文件转化为json文件

python bib2json.py -i ./aaai2021.bib -o ./aaai2021.json

3、把json文件放入项目文件夹中的data文件夹,并在bib_list.txt中加入该文件路径

data/aaai2021.json

参考:https://blog.csdn.net/IT_flying625/article/details/113407745


也可以在官网找到这篇文章

自己参照不同类型bibtex格式将对应内容修改

bibtex格式参考:https://blog.csdn.net/Ryan_lee9410/article/details/106055787

阅读全文

Serial-Assistant-by-CSharp

2022/4/16

C#实现串口通信界面

后续想用unity实现软硬件串口通信,先尝试在Rider中C#实现一个串口通信的界面


安装 .NET Framework

在Rider创建项目的时候会发现,有两栏

一栏是 .NET Core,一栏是 .NET Framework

下面大致讲一下它们的差别

前者完全开源,跨平台,没有designer设计界面

后者基于微软,针对Windows,有designer设计界面

之前学习C#是用的 .NET Core,界面开发用 .NET Framework比较方便,有designer


开始安装 .NET Framework,安装链接:https://dotnet.microsoft.com/zh-cn/download/dotnet-framework

选择开发者工具版,一键安装,安装路径如下,安装后可以查看是否安装成功

C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework

安装完成之后最好重启一下电脑,不然Rider可能识别不到


重启后在Rider中创建项目,选择 .NET Framework,

选择Type>Windows Forms Application

选择Framework>你刚刚安装的版本

修改项目名、路径创建即可

(System.Threading这个命名空间是 .NET Framework4.0后才有的,版本太低可能创建工程后会报错)


代码实现

C#串口通信主要用到了SerialPort,在System.IO.Ports命名空间下

SerialPort在 .NET Framework中自带了,在 .NET Core开发可能要用NuGet安装一下

NuGet是用于.NET开发平台的软件包管理器,我们需要的包可以在这里下载

在Rider使用NuGet,可以参考官方教程:https://www.jetbrains.com/help/rider/Using_NuGet.html

简单来讲:Alt+7,查找、安装


QT也有类似designer的设计界面,但是以往我都是用代码实现

这是第一次接触designer,还是挺方便的

界面开发参考的是一篇用VS实现的博客,讲的挺详细的

链接:https://blog.csdn.net/ba_wang_mao/article/details/113642066


记录一些自己开发过程的笔记:

1.控件事件代码会自动生成抛出异常代码,需要删掉

throw new System.NotImplementedException(); 

2.下拉框用ComboBox实现,Rider默认是没有显示这个控件的,要在右上角Manage Components设置

3.在界面加载时列举电脑的串口,实现如下,其中也包含了string[]转object[],当然不转也没什么问题。

SerialBox.Items.AddRange(SerialPort.GetPortNames().ToArray<object>());

4.SerialPort使用参考官方文档:https://docs.microsoft.com/zh-cn/dotnet/api/system.io.ports.serialport

5.发送和接受文本框采用TextBox,需要设置多行Multiline和竖直翻滚ScrollBars>Vertical

6.接受要注册事件,直接+=函数名

_serial.DataReceived += ReceiveData;

总的开发过程还是和QT很像的,最终实现效果如下:

测试是用一个接受串口数据将原数据发送出来的单片机,收发无误。

阅读全文

SOC

2022/4/12

制作SD卡启动盘

本来是想要试着制作SOC的,SD卡启动盘也制作好了,但是因为很多原因没有进行到最后

这一篇是我制作启动盘的笔记,有些是按我自己的理解实现的

不一定完全对,最终该SD卡启动盘也没有经过验证

所以下述内容不一定正确,仅以此记录


SD卡启动相关知识

启动过程是一个从SD卡逐步搬移到DDR内存,并且运行启动代码进行相关的硬件初始化和软件架构的建立,最终达到运行时稳定状态。u-boot自启动,引导内核启动。



接下来开始制作启动盘!

一、SD卡分区

1.查看设备及分区

插入SD卡,输入

sudo fdisk -l

我的系统上SD卡设备节点是 /dev/sdb

包含一个分区,分区名是sdb1

2.取消所有分区挂载

我只需要删掉一个

sudo umount /dev/sdb1

3.开始分区

sudo fdisk /dev/sdb 

1.输入m查看命令,然后d删除分区

2.输入n添加新分区;接着输入p代表是主分区;然后输入1代表是第一分区;然后直接回车,代表内存从默认地方开始;然后输入+2G,代表分区大小2G。

3.输入n添加新分区;接着输入p代表是主分区;然后输入2代表是第二分区;然后直接回车,代表内存从默认地方开始;然后输入回车,代表分区大小为剩下的内存。

4.输入w保存

4.设置分区格式

先查看以下是否有两个文件

ls -l /dev/sdb*

有可能没有出现/dev/sdb2

则通过以下命令解决(其中b是block型设备文件,8是主设备号,2是辅助设备号)

mknod  /dev/sdb2  b 8 2

确认两个都有之后,设置第一个分区为FAT32格式,第二个分区为EXT3格式,并分别起名为boot和rootfs。

sudo mkfs.vfat -F 32 -n boot /dev/sdb1
sudo mkfs.ext4 -L rootfs /dev/sdb2

二、安装交叉编译器

1.下载

到网址 https://download.friendlyarm.com/nanopineo

toolchain目录下载arm-cortexa9-linux-gnueabihf-4.9.3-20160512.tar.xz

2.创建目录并解压

这里在根目录创建

mkdir -p ~/FriendlyARM/toolchain
tar xf arm-cortexa9-linux-gnueabihf-4.9.3-20160512.tar.xz -C ~/FriendlyARM/toolchain/

3.将编译器的路径写入PATH

用vim编辑.bashrc

vi ~/.bashrc

在末尾添加

export PATH=~/FriendlyARM/toolchain/4.9.3/bin:$PATH
export GCC_COLORS=auto

4.让写入内容生效

. ~/.bashrc

关闭命令行重新打开

5.验证

arm-linux-gcc --version

显示版本号即安装成功


三、编译准备工作

开始编译U-boot和Kernel,我将他们都放在SOC文件夹下

mkdir -p ~/SOC
cd ~/SOC

更换shell中python版本

先查看shell的Python版本是不是Python2 不然后面编译会报错

可以在shell通过以下命令查看python当前版本

我的博客里有一篇change-python-version-in-linux-shell,有讲怎么改版本

python --version

如果不是,参考该链接更改:https://www.bonbonn.shop/2021/12/08/change-python-version-in-linux-shell/


四、编译U-boot

1.下载U-boot源码

git clone https://github.com/friendlyarm/u-boot.git -b sunxi-v2017.x --depth 1

2.编译

apt-get install swig python-dev python3-dev
cd u-boot
make nanopi_h3_defconfig ARCH=arm CROSS_COMPILE=arm-linux-
make ARCH=arm CROSS_COMPILE=arm-linux-

编译成功后会生成文件u-boot-sunxi-with-spl.bin

3.更新到SD卡

确定自己sd卡路径,如上述分区则是/dev/sdb,将文件写入

dd if=u-boot-sunxi-with-spl.bin of=/dev/sdb bs=1024 seek=8

Tips:是写入了无名分区,所以是看不到的,不要惊慌。


五、编译kernel

1.下载Linux内核源码

git clone https://github.com/friendlyarm/linux.git -b sunxi-4.14.y --depth 1

2.编译

需要挺久的

apt-get install u-boot-tools
cd linux
touch .scmversion
make sunxi_defconfig ARCH=arm CROSS_COMPILE=arm-linux-
make zImage dtbs ARCH=arm CROSS_COMPILE=arm-linux-

编译完成后会在arch/arm/boot/目录下生成zImage,并且在arch/arm/boot/dts/目录下生成dtb文件。

3.创建文件夹作为挂载点

如上述分区则是/dev/sdb1

mkdir -p ./SD/boot
sudo mount /dev/sdb1 ./SD/boot

4.更新SD卡上的zImage和dtb文件

cp arch/arm/boot/zImage ./SD/boot
cp arch/arm/boot/dts/sun8i-*-nanopi-*.dtb ./SD/boot

5.取消挂载

sudo umount ./SD/boot


六、编译驱动模块

同上述Linux内核源码路径

1.编译

需要挺久的

cd linux
make modules ARCH=arm CROSS_COMPILE=arm-linux-

2.创建文件夹为挂载点

如上述分区则是/dev/sdb2

mkdir -p ./SD/rootfs
sudo mount /dev/sdb2 ./SD/rootfs

3.更新SD卡上rootfs的驱动模块

cd linux
make modules_install INSTALL_MOD_PATH=/media/SD/rootfs/ ARCH=arm CROSS_COMPILE=arm-linux-

有可能提示权限不够,加了sudo提示arm-linux-gcc不存在

原因是sudo超级用户的环境变量与正常的环境变量不同,而在sudo的环境变量下没有arm-linux-gcc

一种办法就是编辑sudoers文件,取消掉对PATH变量的重置

sudo vi /etc/sudoers

将其中Defaults env_reset 改成 Defaults !env_reset


然后编辑~/.bashrc

vi ~/.bashrc

添加

alias sudo="sudo env PATH=$PATH"

然后关掉命令行,重新进入对应目录执行对应操作

4.取消挂载

sudo umount ./SD/rootfs

参考:http://wiki.friendlyarm.com/wiki/index.php/Building_U-boot_and_Linux_for_H5/H3/H2%2B/zh

阅读全文

learn_CSharp

2022/4/12

学习C#

想用unity开发一些有意思的上位机软件,先学习一下C#

环境搭建

我选择在JetBrains的Rider进行C#的学习,直接在Jetbrains Toolbox安装

C#依赖于 .Net ,需要安装DotNet-SDK,链接:https://dotnet.microsoft.com/zh-cn/download

一键安装,环境变量也配置好了,可以打开命令行cmd确认是否安装完成

dotnet --version

打开Rider,新建项目,选择 .NET/ .NET Core>Console Application,然后起个名字,指定路径

自动出现HelloWorld程序,编译运行即可

Console.WriteLine("Hello, World!");

学习笔记

C# Language Specification:https://docs.microsoft.com/en-us/dotnet/csharp/

中文版C#文档的:https://docs.microsoft.com/zh-cn/dotnet/csharp/

C语言中文网:http://c.biancheng.net/csharp/


以下并没有包含完整C#入门知识点,只是我挑选着记录的笔记

一些概念

首先我们创建项目时就有两个概念:项目(Project)和解决方案(Solution),两者默认同名

项目可以理解成你最后要实现的总功能,比如Web应用程序。

解决方案可以理解成存放不同功能的容器,当开发的总项目较为复杂时会包含很多功能

用Rider创建项目后会自带Program.cs,这个文件是程序的入口。


程序入口

在学习之前我有大致了解,C#是完全面向对象的语言。

在测试HelloWorld的时候就有疑问,为什么不需要在类里放Main函数作为程序入口点

搜索后发现,自C# 9起,使用顶级语句可以省略Main方法,编译器会自动生成类和Main入口点


HelloWorld

以HelloWorld为例,实现多种方式运行

首先最简单的就是使用顶级语句:

//Console是命名空间System下的一个类 用于命令行输出 我学习时已经不需要using System;了
Console.WriteLine("Hello, World!");

其次是使用类的Main作为入口,参考教程中实现如下:

namespace LearnCsharp
{
    class HelloWorld
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello, World!");
        }
    }
}

关于namespace,也可以写成文件范围的命名空间声明

这样整个文件都属于一个命名空间,常用于一个文件中有多个类的情况,如下:

namespace LearnCsharp;
class HelloWorld
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello, World!");
    }
}

这个代码是可以编译运行的,但是Rider中会有警告

1.会提示类没有实例化,Class ‘HelloWorld’ is never instantiated

2.会提示Main缺少private,Inconsistent modifiers style: missing ‘private’ modifier


下面是我觉得实现起来思路比较顺的一种写法,

新建一个类,源文件名为HelloWorld.cs

namespace LearnCsharp;

public static class HelloWorld
{
    public static void Print()
    {
        Console.WriteLine("Hello, World!");
    }
}

Program.cs中调用

using LearnCsharp;

HelloWorld.Print();

static

static表示静态

不需要实例化就可以调用的方法为静态方法,静态方法中只能使用静态类成员

一般不需要实例化的类就定义为静态类,其所有方法一定是静态方法


字符串

字符串采用string类型,常使用const描述符。

字符串拼接可以使用+,可以直接拼接数字。

字符串前缀@,表示后续字符串不转义,常用于路径前,相当于python的字符串前缀r

字符串前缀$,可用于字符串格式化,相当于python的字符串前缀f


访问修饰符和修饰符

访问修饰符:

public:可以被任何代码访问

private(默认):可以被同一类中代码访问

internal:可以被同一项目访问

protected:只能由类或派生类中的代码访问


修饰符记录一下readonly和const

const修饰的在声明时就要设置值

而readonly修饰的可以在类的构造函数时设置值


定义字段时顺序如下:

访问修饰符 修饰符 数据类型 字段名;


get和set

首先介绍一下字段与属性

字段:一般是private,用于内部数据交互,若需要提供给外部时,需要封装为属性

属性:一般是public,在面向对象设计中主要使用属性描述对象的静态特征


get和set就是可以将字段封装为属性,可以设置获得值以及设置值时做的操作

C#中的get和set类似于Python中@property和setter,实现如下:

public class Student
{
    private int _id = 1;
    public int Id
    {
        get
        {
            return _id;
        }
        set
        {
            _id = value;
        }
    }
}

如果不做逻辑判断,可见上述字段只是作为一个中间变量

为了简化,可以采用自动属性设置方式,写成如下:

public class Student
{
    public int Id { get; set; } = 1;//为了区分字段和属性 应该避免写成public int Id = 1;
    // public int Id { private get; set; } = 1; //只写
    // public int Id { get;  } = 1; //只读 
}

属性的作用:

1.可以加入判断,避免非法数据

2.可以根据情况设置只读或者只写

3.调用属性时进行运算,不需要对字段进行操作


构造和析构

构造方法前需要加访问修饰符,析构则不需要。

我试了一下,写个析构,断点调试,并没有运行。

网上有网友说,C#析构函数不一定会执行,官方不推荐在析构函数释放堆栈。


引用参数和输出参数

这两个参数都是按地址传递,使用后参数的值会改变,感觉类似C++的引用

引用参数ref,方法定义与方法调用都需要加上ref,ref参数必须初始化,例如两数交换实现如下:

namespace LearnCsharp;

public static class Operation
{
    public static void Swap(ref int  a, ref int  b)
    {
        a ^= b;
        b ^= a;
        a ^= b;
    }
}
using LearnCsharp;

var a = 5;
var b = 10;
Operation.Swap(ref a,ref b);
Console.WriteLine(a);
Console.WriteLine(b);

输出参数out,方法定义与方法调用都需要加上out,out参数不能初始化,例如传入函数初始化:

namespace LearnCsharp;

public static class Number
{
    public static void Init(out int  a)
    {
        a = 5;
    }
}
using LearnCsharp;

Number.Init(out var a);
Console.WriteLine(a);

lambda

格式为:

访问修饰符 修饰符 返回值类型 方法名(参数列表) => 表达式;

例如实现两数相加:

public static int Add(int a, int b) => a + b;

部分类

在class之前加上partial的类是部分类,部分类就是一个类的一个部分,

所有部分类合起来就是完整的一个类,哈哈哈哈感觉有点废话


装箱和拆箱

将值类型转换为引用类型的操作称为装箱,将引用类型转换成值类型称为拆箱

例如int转string称为装箱,string转int称为拆箱

//装箱
const int  n = 100;
var s = n.ToString();
Console.WriteLine(s);
//拆箱
const string s = "100";
var n = int.Parse(s);
Console.WriteLine(n);

数组

string[] stringArray = {"123", "456", "789"}; //数组声明 
Console.WriteLine(stringArray[0]); //下标访问
Console.WriteLine(stringArray.Length); //数组长度

var numArray = new int[5];//初始化五个零的数组
Console.WriteLine(numArray[0]); //下标访问
Console.WriteLine(numArray.Length); //数组长度

foreach

foreach用于遍历,使用起来类似Python的for循环,例如遍历数组:

int[] numArray = {1,2,3,4,5,6,7,8,9,0};
foreach(var number in numArray)
{
    Console.WriteLine(number);
}

枚举

枚举和C语言类似,但是他每一个枚举值都是表示字符串,可以通过转换获得对应的值

所以我们在代码中也能喜闻乐见的看到,中文没有被扩在引号内而且不会报错了

namespace LearnCsharp;
public enum Color : int
{
    红,
    蓝,
    黄
}
using LearnCsharp;


Console.WriteLine((int)Color.红);
Console.WriteLine((int)Color.蓝);
Console.WriteLine((int)Color.黄);

Object类

Object类是所有类的父类,类定义时默认继承Object类

Object 类中提供了 4 个常用的方法

Equals用于判断是否引用了同一个对象

GetHashCode用于返回当前实例的哈希值

GetType用于返回当前实例的类型

ToString用于返回一个对象实例的字符串


继承

继承父类只需要在定义类时在它后面加个冒号以及父类,调用父类的方法需要用base关键字

virtual 方法表示将会在继承后重写

override 修饰符用于扩展基类 virtual 方法,相当于重新定义父类中方法的内容

new 修饰符用于隐藏可访问的基类方法,相当于定义新方法


abstract修饰符代表抽象的,

virtual和abstract都是用来修饰父类的,子类重新定义覆盖父类。

virtual修饰的方法一定要有实现,而abstract一定没有实现

virtual可以被子类重写,而abstract一定被子类重写


sealed 修饰符代表密封的,只能出现在子类,并且是重写父类方法,必须与 override 关键字一起使用

sealed修饰的类不能被继承,修饰的方法不能被重写


接口

接口用interface修饰符来定义,接口名称常用I开头的单词构成。

接口中不能有字段,接口中不能有方法实现,并且成员不能被修饰符修饰,接口的方法通过类继承来实现。

关于接口和抽象类,有个比喻很好理解:

飞机和鸟都会飞,他们都继承了接口飞,但直升飞机属于飞机抽象类,鸽子属于鸟抽象类。


集合

ArrayList是动态数组,与数组操作类似,但是功能丰富很多

Queue是队列,元素从尾部插入,从头部移除

Stack是栈,元素先进入的到最底部,也就是先进后出

Hashtable是哈希表(散列表),存放键值对

SortedList是有序列表,存放键值对,按值排序

使用之前需要using System.Collections;

阅读全文